package com.dbflow5.processor.definition.column;

import com.dbflow5.processor.SQLiteHelper;
import com.dbflow5.processor.utils.CodeExtensions;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.NameAllocator;
import com.squareup.javapoet.TypeName;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Description: Provides structured way to combine ForeignKey for both SQLiteStatement and ContentValues
 * bindings.
 *
 * @author Andrew Grosner (fuzz)
 */
public class ForeignKeyAccessCombiner {
    private final ColumnAccessor fieldAccessor;
    public List<ForeignKeyAccessField> fieldAccesses = new ArrayList<>();

    public ForeignKeyAccessCombiner(ColumnAccessor fieldAccessor) {
        this.fieldAccessor = fieldAccessor;
    }

    public void addCode(CodeBlock.Builder code, AtomicInteger index, boolean useStart, boolean defineProperty) {
        CodeBlock modelAccessBlock = fieldAccessor.get(ColumnAccessor.modelBlock);
        code.beginControlFlow("if ($L != null)", modelAccessBlock);
        CodeBlock.Builder nullAccessBlock = CodeBlock.builder();
        for (int i=0;i<fieldAccesses.size();i++) {
            ForeignKeyAccessField field = fieldAccesses.get(i);
            field.addCode(code, index.get(), modelAccessBlock, useStart, defineProperty);
            field.addNull(nullAccessBlock, index.get(), useStart);

            // do not increment last
            if (i < fieldAccesses.size() - 1) {
                index.incrementAndGet();
            }
        }
        code.nextControlFlow("else")
                .add(nullAccessBlock.build().toString())
                .endControlFlow();
    }

    public static class ForeignKeyAccessField {
        private final String columnRepresentation;
        private final ColumnAccessCombiner columnAccessCombiner;
        private CodeBlock defaultValue;

        public ForeignKeyAccessField(String columnRepresentation, ColumnAccessCombiner columnAccessCombiner, CodeBlock defaultValue) {
            this.columnRepresentation = columnRepresentation;
            this.columnAccessCombiner = columnAccessCombiner;
            this.defaultValue = defaultValue;
        }

        public void addCode(CodeBlock.Builder code, int index, CodeBlock modelAccessBlock,
                    boolean useStart, boolean defineProperty) {
            columnAccessCombiner.addCode(code, useStart? columnRepresentation : "", defaultValue, index, modelAccessBlock, defineProperty);
        }

        public void addNull(CodeBlock.Builder code, int index, boolean useStart) {
            columnAccessCombiner.addNull(code, useStart? columnRepresentation : "", index);
        }
    }

    public static class ForeignKeyLoadFromCursorCombiner {
        public List<PartialLoadFromCursorAccessCombiner> fieldAccesses = new ArrayList<>();

        private final ColumnAccessor fieldAccessor;
        private final TypeName referencedTypeName;
        private final TypeName referencedTableTypeName;
        private final boolean isStubbed;
        private final NameAllocator nameAllocator;

        public ForeignKeyLoadFromCursorCombiner(ColumnAccessor fieldAccessor, TypeName referencedTypeName, TypeName referencedTableTypeName,
                                                boolean isStubbed, NameAllocator nameAllocator) {
            this.fieldAccessor = fieldAccessor;
            this.referencedTypeName = referencedTypeName;
            this.referencedTableTypeName = referencedTableTypeName;
            this.isStubbed = isStubbed;
            this.nameAllocator = nameAllocator;
        }

        public void addCode(CodeBlock.Builder code, AtomicInteger index) {
            CodeBlock.Builder ifChecker = CodeBlock.builder();
            CodeBlock.Builder setterBlock = CodeBlock.builder();

            if (!isStubbed) {
                setterBlock.add("$T.select().from($T.class).where()",
                        com.dbflow5.processor.ClassNames.SQLITE, referencedTypeName);
            } else {
                CodeExtensions.statement(setterBlock, fieldAccessor.set(CodeBlock.of("new $T()", referencedTypeName), ColumnAccessor.modelBlock, false));
            }
            for (int i=0;i<fieldAccesses.size();i++) {
                PartialLoadFromCursorAccessCombiner it = fieldAccesses.get(i);
                it.addRetrieval(setterBlock, index.get(), referencedTableTypeName, isStubbed, fieldAccessor, nameAllocator);
                it.addColumnIndex(code, index.get(), referencedTableTypeName, nameAllocator);
                it.addIndexCheckStatement(ifChecker, index.get(), referencedTableTypeName,
                        i == fieldAccesses.size() - 1, nameAllocator);

                if (i < fieldAccesses.size() - 1) {
                    index.incrementAndGet();
                }
            }

            if (!isStubbed) setterBlock.add("\n.querySingle(wrapper)");

            code.beginControlFlow("if ($L)", ifChecker.build());
            if (!isStubbed) {
                CodeExtensions.statement(code, fieldAccessor.set(setterBlock.build(), ColumnAccessor.modelBlock, false));
            } else {
                code.add(setterBlock.build());
            }
            CodeExtensions.statement(code.nextControlFlow("else"), fieldAccessor.set(CodeBlock.of("null"), ColumnAccessor.modelBlock, false))
                    .endControlFlow();
        }
    }

    public static class PartialLoadFromCursorAccessCombiner {
        private final String columnRepresentation;
        private final String propertyRepresentation;
        private final TypeName fieldTypeName;
        private final boolean orderedCursorLookup;
        private final ColumnAccessor fieldLevelAccessor;
        private final ColumnAccessor subWrapperAccessor;
        private final TypeName subWrapperTypeName;

        private CodeBlock indexName = null;

        public PartialLoadFromCursorAccessCombiner(String columnRepresentation, String propertyRepresentation, TypeName fieldTypeName, boolean orderedCursorLookup,
                                                   ColumnAccessor fieldLevelAccessor, ColumnAccessor subWrapperAccessor, TypeName subWrapperTypeName) {
            this.columnRepresentation = columnRepresentation;
            this.propertyRepresentation = propertyRepresentation;
            this.fieldTypeName = fieldTypeName;
            this.orderedCursorLookup = orderedCursorLookup;
            this.fieldLevelAccessor = fieldLevelAccessor;
            this.subWrapperAccessor = subWrapperAccessor;
            this.subWrapperTypeName = subWrapperTypeName;
        }

        private CodeBlock getIndexName(int index, NameAllocator nameAllocator, TypeName referencedTypeName) {
            if (indexName == null) {
                if (!orderedCursorLookup) {
                    // post fix with referenced type name simple name
                    indexName = CodeBlock.of(nameAllocator.newName("index_"+columnRepresentation+"_" +
                            ((referencedTypeName instanceof ClassName)? ((ClassName) referencedTypeName).simpleName() : ""), columnRepresentation));
                } else {
                    CodeBlock.of(String.valueOf(index));
                }
            }
            return indexName;
        }

        public void addRetrieval(CodeBlock.Builder code, int index, TypeName referencedTableTypeName,
                                 boolean isStubbed, ColumnAccessor parentAccessor, NameAllocator nameAllocator) {
            CodeBlock cursorAccess = CodeBlock.of("cursor.$L($L)",
                    SQLiteHelper.getMethod(subWrapperTypeName != null? subWrapperTypeName : fieldTypeName),
            getIndexName(index, nameAllocator, referencedTableTypeName));

            CodeBlock fieldAccessBlock = cursorAccess;
            if(subWrapperAccessor != null && subWrapperAccessor.set(cursorAccess, null, false) != null){
                fieldAccessBlock = subWrapperAccessor.set(cursorAccess, null, false);
            }

            if (!isStubbed) {
                code.add(CodeBlock.of("\n.and($T.$L.eq($L))",
                        referencedTableTypeName, propertyRepresentation, fieldAccessBlock));
            } else if (fieldLevelAccessor != null) {
                CodeExtensions.statement(code, fieldLevelAccessor.set(fieldAccessBlock, parentAccessor.get(ColumnAccessor.modelBlock), false));
            }
        }

        public void addColumnIndex(CodeBlock.Builder code, int index, TypeName referencedTableTypeName, NameAllocator nameAllocator) {
            if (!orderedCursorLookup) {
                CodeExtensions.statement(code, CodeBlock.of("int $L = cursor.getColumnIndex($S)",
                        getIndexName(index, nameAllocator, referencedTableTypeName), columnRepresentation));
            }
        }

        public void addIndexCheckStatement(CodeBlock.Builder code, int index, TypeName referencedTableTypeName,
                                           boolean isLast, NameAllocator nameAllocator) {
            CodeBlock indexName = getIndexName(index, nameAllocator, referencedTableTypeName);
            if (!orderedCursorLookup) code.add(indexName + " != -1 && ");

            code.add("!cursor.isNull("+indexName+")");

            if (!isLast) code.add(" && ");
        }
    }
}

